package com.sunday.redis.lua.core;

import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.StringRedisTemplate;

import java.lang.reflect.Type;
import java.util.Collection;
import java.util.Collections;
import java.util.concurrent.TimeUnit;

/**
 * redis 操作类
 * https://docs.spring.io/spring-data/data-redis/docs/current/reference/html/#get-started
 * <p>
 * 有一个spring-boot-starter-data-redis“Starter”可以方便地收集依赖关系。默认情况下，
 * 它使用Lettuce。该启动程序可以处理传统应用程序和响应式应用程序。
 * <p>
 * http://redis.io/commands/setex
 * void set(K key, V value, long timeout, TimeUnit unit);
 * 必须注意 在spring data 封装的 set命令中存在 设置超时时间的方法
 * 但是采用的是 setEX能力，此能力不可使用在分布式锁中,他会使信息反复覆盖，无法做到数据锁定的能力
 *
 * @author zsy
 * @since 2022/10/17
 */
@Slf4j
public record RedisLuaTemplate(StringRedisTemplate stringRedisTemplate) implements RedisLuaOperations {

    @Override
    public Long incrementWithLimit(String key, long upper) {
        return incrementWithLimit(key, 1, upper);
    }

    /**
     * 指定数量累加, 并判定上限
     * 场景: 用户部分功能的数量限制，防止设置超限
     *
     * @param key
     * @param delta 数量
     * @param upper 上限
     * @return {@link Long} 到达上限返回null，否则返回累加后的值
     */
    @Override
    public Long incrementWithLimit(String key, long delta, long upper) {
        return stringRedisTemplate.execute(RedisScriptLua.INCREMENT_WITH_LIMIT, Collections.singletonList(key), String.valueOf(delta), String.valueOf(upper));
    }

    @Override
    public Long decrementWithFloor(String key) {
        return decrementWithFloor(key, 1);
    }

    @Override
    public Long decrementWithFloor(String key, long delta) {
        return decrementWithFloor(key, delta, 0);
    }

    /**
     * 指定数量扣减, 并判定下限
     * 场景: 用户余额指定数量扣减
     *
     * @param key
     * @param delta 数量
     * @param lower 下限
     * @return {@link Long} 到达下限返回null，否则返回递减后的值
     */
    @Override
    public Long decrementWithFloor(String key, long delta, long lower) {
        return stringRedisTemplate.execute(RedisScriptLua.DECREMENT_WITH_FLOOR, Collections.singletonList(key), String.valueOf(delta), String.valueOf(lower));
    }

    @Override
    public Long incrementWithinPeriod(String key, long upper, long timePeriodInSeconds) {
        return incrementWithinPeriod(key, 1, upper, timePeriodInSeconds);
    }

    /**
     * 在指定时间范围内按照指定数量累加，并判定上限
     * 场景: 分布式消息递送的频次限制，请求的TPS限制
     *
     * @param key
     * @param delta               数量
     * @param upper               上限
     * @param timePeriodInSeconds 过期时间/S
     * @return {@link Long} 返回应休眠的时间（ms毫秒），返回0未超限无需休眠
     */
    @Override
    public Long incrementWithinPeriod(String key, long delta, long upper, long timePeriodInSeconds) {
        return stringRedisTemplate.execute(RedisScriptLua.INCREMENT_WITHIN_PERIOD, Collections.singletonList(key), String.valueOf(delta), String.valueOf(upper), String.valueOf(timePeriodInSeconds));
    }

    @Override
    public Boolean easyIncrementWithinPeriod(String key, long upper, long timePeriodInSeconds) {
        return easyIncrementWithinPeriod(key, 1, upper, timePeriodInSeconds);
    }

    /**
     * 时间范围内超限判定 (简易版，不进行TTL控制)
     * 场景: 分布式消息递送的频次限制，请求的TPS限制
     *
     * @param key
     * @param delta               数量
     * @param upper               上限
     * @param timePeriodInSeconds 过期时间/S
     * @return {@link Boolean} 返回上限情况，true正常累加，false超限
     */
    @Override
    public Boolean easyIncrementWithinPeriod(String key, long delta, long upper, long timePeriodInSeconds) {
        return stringRedisTemplate.execute(RedisScriptLua.EASY_INCREMENT_WITHIN_PERIOD, Collections.singletonList(key), String.valueOf(delta), String.valueOf(upper), String.valueOf(timePeriodInSeconds));
    }

    /**
     * 在期限内累加，返回当前计数数量
     * 场景: 仅判定累加数量，不进行额外的控制，其他由业务自行判定
     *
     * @param key
     * @param timePeriodInSeconds
     * @return {@link Long} 返回当前计数数量
     */
    @Override
    public Long incrWithinPeriod(String key, long timePeriodInSeconds) {
        return stringRedisTemplate.execute(RedisScriptLua.INCR_WITHIN_PERIOD, Collections.singletonList(key), String.valueOf(timePeriodInSeconds));
    }

    @Override
    public Boolean decrementWithinPeriod(String key) {
        return decrementWithinPeriod(key, 10);
    }

    @Override
    public Boolean decrementWithinPeriod(String key, long maximumTimeMillis) {
        return decrementWithinPeriod(key, 0, maximumTimeMillis);
    }

    @Override
    public Boolean decrementWithinPeriod(String key, long lower, long maximumTimeMillis) {
        return decrementWithinPeriod(key, 1, lower, maximumTimeMillis);
    }

    /**
     * 在允许操作的时间范围内进行扣减，并验证下限
     * 场景: 分布式消息递送的频次,发生异常时,计数归还动作
     *
     * @param key
     * @param delta             数量
     * @param lower             下限
     * @param maximumTimeMillis 限制时间范围/MS
     * @return {@link Boolean} 返回下限情况，true正常递减，false超限
     */
    @Override
    public Boolean decrementWithinPeriod(String key, long delta, long lower, long maximumTimeMillis) {
        return stringRedisTemplate.execute(RedisScriptLua.DECREMENT_WITHIN_PERIOD, Collections.singletonList(key), String.valueOf(delta), String.valueOf(lower), String.valueOf(maximumTimeMillis));
    }

    /**
     * 分时段速率控制 (分钟/小时/天)
     * 使用场景：在指定时间内，手机号码超过最大次数，不考虑次数累加带来的影响，并返回当前计数数量
     *
     * @param key
     * @param maxMinute 每分钟上限
     * @param maxHour   每小时上限
     * @param maxDay    每天上限
     * @return {@link Long} 0 未超限 1 每分钟 2 每小时 3 每天
     */
    @Override
    public Long calculateSegmentRate(String key, long maxMinute, long maxHour, long maxDay) {
        return stringRedisTemplate.execute(RedisScriptLua.CALCULATE_SEGMENT_RATE, Collections.singletonList(key), String.valueOf(maxMinute), String.valueOf(maxHour), String.valueOf(maxDay));
    }

    /**
     * 递增并判定上限，每次都刷新过期时间
     * 场景：下载计数，每次刷新过期时间
     *
     * @param key
     * @param maxLimit    上限
     * @param expireInSec 过期时间/S
     * @return {@link Boolean} 返回是否超限，true未超限，false超限
     */
    @Override
    public Boolean incrementAndCheckMaxWithRefresh(String key, long maxLimit, long expireInSec) {
        return stringRedisTemplate.execute(RedisScriptLua.INCR_CHECK_MAX_AND_REFRESH, Collections.singletonList(key), String.valueOf(maxLimit), String.valueOf(expireInSec));
    }

    /**
     * 更新值，不更新过期时间
     * 场景： 仅更新值内容
     *
     * @param key
     * @param value       重置值
     * @param expireInSec 过期时间/S
     * @return {@link Long} 返回剩余过期时间(ms毫秒)
     */
    @Override
    public <T> Long updateValueKeepTTL(String key, T value, long expireInSec) {
        return stringRedisTemplate.execute(RedisScriptLua.UPDATE_VALUE_KEEP_TTL, Collections.singletonList(key), toString(value), String.valueOf(expireInSec));
    }

    /**
     * 值相同刷新过期时间（key 不存在设置失败）
     * 场景： 延长数据的存活时间，排他锁能力
     *
     * @param key
     * @param value
     * @param expireInSec 过期时间/S
     * @return {@link Boolean} 返回是否刷新成功，true刷新成功，false刷新失败
     */
    @Override
    public <T> Boolean refreshExpiryIfValueMatch(String key, T value, long expireInSec) {
        return stringRedisTemplate.execute(RedisScriptLua.VALUE_EQ_AND_EXPIRE, Collections.singletonList(key), toString(value), String.valueOf(expireInSec));
    }

    /**
     * https://redis.io/commands/del/
     * DEL key [key ...]
     */
    @Override
    public Long delete(Collection<String> keys) {
        return stringRedisTemplate.delete(keys);
    }

    @Override
    public Boolean delete(String key) {
        return stringRedisTemplate.delete(key);
    }

    /**
     * https://redis.io/commands/set/
     * SET key value [NX | XX] [GET] [EX seconds | PX milliseconds | EXAT unix-time-seconds | PXAT unix-time-milliseconds | KEEPTTL]
     * <p>
     * EX seconds -- 设置指定的过期时间，以秒为单位。
     * PX 毫秒——设置指定的过期时间，以毫秒为单位。
     * EXAT timestamp-seconds -- 设置密钥过期的指定 Unix 时间，以秒为单位。
     * PXAT timestamp-milliseconds -- 设置密钥过期的指定 Unix 时间，以毫秒为单位。
     * NX-- 仅当密钥不存在时才设置它。
     * XX-- 仅当密钥已存在时才设置它。
     * KEEPTTL-- 保留与密钥关联的生存时间。
     * GET-- 返回存储在 key 中的旧字符串，如果 key 不存在，则返回 nil。SET如果存储在 key 的值不是字符串，则返回并中止错误。
     * <p>
     * 从 Redis 版本 2.6.12 开始：添加了、EX和PX选项。NXXX
     * 从 Redis 版本 6.0.0 开始：添加了该KEEPTTL选项。
     * 从 Redis 版本 6.2.0 开始：添加了GET,EXAT和PXAT选项。
     * 从 Redis 版本 7.0.0 开始：允许NX和GET选项一起使用。
     *
     * @param key
     * @param value
     */
    @Override
    public <T> void set(String key, T value) {
        stringRedisTemplate.opsForValue().set(key, toString(value));
    }

    /**
     * SET key value NX EX seconds
     *
     * @param key
     * @param value
     * @param expireInSec
     * @return {@link Boolean}
     */
    @Override
    public <T> Boolean setIfAbsent(String key, T value, long expireInSec) {
        return stringRedisTemplate.opsForValue().setIfAbsent(key, toString(value), expireInSec, TimeUnit.SECONDS);
    }

    /**
     * SET key value XX EX seconds
     *
     * @param key
     * @param value
     * @param expireInSec
     * @return {@link Boolean}
     */
    @Override
    public <T> Boolean setIfPresent(String key, T value, long expireInSec) {
        return stringRedisTemplate.opsForValue().setIfPresent(key, toString(value), expireInSec, TimeUnit.SECONDS);
    }

    /**
     * https://redis.io/commands/setex/
     * SETEX key seconds value
     *
     * @param key
     * @param value
     * @param expireInSec
     */
    @Override
    public <T> void setEx(String key, T value, long expireInSec) {
        stringRedisTemplate.opsForValue().set(key, toString(value), expireInSec, TimeUnit.SECONDS);
    }

    /**
     * https://redis.io/commands/get/
     * GET key
     *
     * @param key
     * @return {@link String}
     */
    @Override
    public String get(String key) {
        return stringRedisTemplate.opsForValue().get(key);
    }

    @Override
    public <T> T get(String key, Class<T> clazz) {
        return fromJson(stringRedisTemplate.opsForValue().get(key), clazz);
    }

    @Override
    public <T> T get(String key, Type type) {
        return fromJson(stringRedisTemplate.opsForValue().get(key), type);
    }

    /**
     * https://redis.io/commands/ttl/
     * TTL key
     *
     * @param key
     * @return {@link Long}
     */
    @Override
    public Long ttl(String key) {
        return stringRedisTemplate.getExpire(key);
    }

    /**
     * https://redis.io/commands/pttl/
     * PTTL key
     *
     * @param key
     * @return {@link Long}
     */
    @Override
    public Long pTtl(String key) {
        return stringRedisTemplate.getExpire(key, TimeUnit.MILLISECONDS);
    }

}
